﻿using System.Collections;
using System.Collections.Generic;
using System.Data.Metadata.Edm;
using System.Data.Objects;
using System.Linq;
using System.Text;
using System;
using System.Diagnostics;

namespace EntityGraph.SQL
{
    internal class EntityGraphPath : IEnumerable<EntityGraphPathNode>
    {
        List<EntityGraphPathNode> nodes = new List<EntityGraphPathNode>();
        /// <summary>
        /// Returns a node in path that is the owning type of the incoming edge of node.
        /// E.g., for a path PatientEvent, Cancer,  BreastCancer, patient, this method would return PatientEvent
        /// since that is the type that declares the property Patient Patient{get;}.
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private EntityGraphPathNode GetOwningNode(EntityGraphPathNode node)
        {
            foreach(var owner in this)
            {
                if(owner.Type == node.InEdge.EdgeInfo.DeclaringType)
                {
                    return owner;
                }
            }
            throw new Exception("Owning node not found");
        }
        private SqlEqualityExpression Edge2SqlExpression(EntityGraphPathNode rightNode)
        {
            return Edge2SqlExpression(rightNode, null);
        }
        internal SqlConstantExpression Edge2Value(EntityGraphPathNode rightNode, object entity)
        {
            var leftNode = GetOwningNode(rightNode);
            var left = leftNode.TypeTree.EntityType;
            var right = rightNode.TypeTree.EntityType;

            var nProp = left.NavigationProperties.Single(nprop => nprop.Name == leftNode.OutEdge.EdgeInfo.Name);
            AssociationType associationType = (AssociationType)nProp.RelationshipType;
            EntityType fromType = null;
            EntityType toType = null;
            EdmProperty fromProperty = null;
            EdmProperty toProperty = null;
            if(associationType.IsForeignKey)
            {
                var constraint = associationType.ReferentialConstraints.First();
                fromType = constraint.FromRole.GetEntityType();
                toType = constraint.ToRole.GetEntityType();
                fromProperty = constraint.FromProperties.First();
                toProperty = constraint.ToProperties.First();
            }
            else
            {
                fromType = ((RefType)associationType.KeyMembers[0].TypeUsage.EdmType).ElementType as EntityType;
                fromProperty = fromType.KeyMembers[0] as EdmProperty;
                toType = ((RefType)associationType.KeyMembers[1].TypeUsage.EdmType).ElementType as EntityType;
                toProperty = fromType.KeyMembers[0] as EdmProperty;
            }
            var leftTableName = left.Name;
            var leftPropertyName = left == toType ? toProperty.Name : fromProperty.Name;
            var rightTableName = right.Name;
            var rightPropertyName = right == toType ? toProperty.Name : fromProperty.Name;
            var declaringType = leftNode.OutEdge.EdgeInfo.DeclaringType;
            var foreignKeyValue = declaringType.GetProperty(leftPropertyName).GetValue(entity, null);
            return new SqlConstantExpression(foreignKeyValue);
        }

        private SqlEqualityExpression Edge2SqlExpression(EntityGraphPathNode rightNode, object entity)
        {
            var leftNode = GetOwningNode(rightNode);
            var left = leftNode.TypeTree.EntityType;
            var right = rightNode.TypeTree.EntityType;

            var nProp = left.NavigationProperties.Single(nprop => nprop.Name == leftNode.OutEdge.EdgeInfo.Name);
            AssociationType associationType = (AssociationType)nProp.RelationshipType;
            EntityType fromType = null;
            EntityType toType = null;
            EdmProperty fromProperty = null;
            EdmProperty toProperty = null;
            if(associationType.IsForeignKey)
            {
                var constraint = associationType.ReferentialConstraints.First();
                fromType = constraint.FromRole.GetEntityType();
                toType = constraint.ToRole.GetEntityType();
                fromProperty = constraint.FromProperties.First();
                toProperty = constraint.ToProperties.First();
            }
            else
            {
                fromType = ((RefType)associationType.KeyMembers[0].TypeUsage.EdmType).ElementType as EntityType;
                fromProperty = fromType.KeyMembers[0] as EdmProperty;
                toType = ((RefType)associationType.KeyMembers[1].TypeUsage.EdmType).ElementType as EntityType;
                toProperty = fromType.KeyMembers[0] as EdmProperty;
            }
            var leftTableName = left.Name;
            var leftPropertyName = left == toType ? toProperty.Name : fromProperty.Name;
            var rightTableName = right.Name;
            var rightPropertyName = right == toType ? toProperty.Name : fromProperty.Name;
            if(entity != null)
            {
                var declaringType = leftNode.OutEdge.EdgeInfo.DeclaringType;
                var foreignKeyValue = declaringType.GetProperty(leftPropertyName).GetValue(entity, null);
                return new SqlEqualityExpression(
                        new SqlColumnExpression(new SqlColumn(rightTableName, rightPropertyName)),
                    //                        new SqlConstantExpression(foreignKeyValue)
                    new SQLParameter(rightTableName + rightPropertyName));
            }
            else
            {
                return new SqlEqualityExpression(
                        new SqlColumnExpression(new SqlColumn(leftTableName, leftPropertyName)),
                        new SqlColumnExpression(new SqlColumn(rightTableName, rightPropertyName)));
            }
        }
        /// <summary>
        /// Translates this EntityGraphPath to SQL starting from the provided entity.
        /// The entity is used to restict the query using a Where statement,
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="entity"></param>
        /// <returns></returns>
        public SqlSelectFromTable ToSql(ObjectContext ctx, object entity)
        {
            Debug.Assert(entity != null);
            var sql = ToSql(ctx, true);

            var right = this[1];
            var expr = Edge2SqlExpression(right, entity);
            var columnExpr = (SqlColumnExpression)expr.Lhs;
            if(this.Count() == 2)
            {
                // Add type id to the RHS of this onCondition because TypeTree2Sql appends type ids to columns to prevent name clashes
                columnExpr.Column.Name += right.TypeTree.TypeId;
            }
            else
            {
            }
//            var parameter = new SQLParameter(columnExpr.Column.Table + columnExpr.Column.Name);
//            var where = new SqlWhereExpression(new SqlEqualityExpression(expr.Lhs, parameter));
//            var binding = new SQLParameterBinding(parameter, expr.Rhs as SqlConstantExpression);
//            var selectWithBinding = new SqlSelectFromTableWithParameterBinding(sql.FromTable, sql.Table, where, binding);
            sql.Where = new SqlWhereExpression(expr);
            return sql;
//            return selectWithBinding;
        }
        /// <summary>
        /// Translates this EntityGraphPath to SQL.
        /// </summary>
        /// <param name="ctx"></param>
        /// <returns></returns>
        public SqlSelectFromTable ToSql(ObjectContext ctx)
        {
            return ToSql(ctx, false);
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="ctx"></param>
        /// <param name="skipFirstNode">Indicates that the first node should not be included in the cosntructed
        /// join. This is in case this join is on a single row, rather than a complete table.
        /// </param>
        /// <returns></returns>
        private SqlSelectFromTable ToSql(ObjectContext ctx, bool skipFirstNode)
        {
            int startIdx = 0;
            // Skip the first node of a path if that node is already available
            if(skipFirstNode)
            {
                startIdx = 1;
            }

            var startNode = nodes[startIdx];
            var endNode = nodes.Last();
            // If the path consists of only one rootNode, no joins are needed.
            if(startNode == endNode)
            {
                // Check if .Root is needed here. That is if TypeTree.Root == TypeTree.RequestedType
                System.Diagnostics.Debug.Assert(endNode.TypeTree.EntityType.Name == endNode.Type.Name);
                var table = new SqlNesting(endNode.TypeTree.Root.ToSql(), endNode.Type.Name);
                return new SqlSelectFromTable(table.AsName, table, null);
            }
            // Check if .Root is needed here. That is if TypeTree.Root == TypeTree.RequestedType
            SqlTable join = new SqlTable(startNode.TypeTree.TableName, startNode.Type.Name, null);
            SqlEqualityExpression onCondition = null;
            for(int i = startIdx + 1; i + 1 < nodes.Count(); i++)
            {
                var node = nodes[i];
                if(node.InEdge == null) // this rootNode is part of an inheritance chain
                {
                    var left = nodes[i - 1].TypeTree;
                    var right = node.TypeTree;
                    var leftKey = left.EntityType.KeyMembers.First().Name;
                    var rightKey = right.EntityType.KeyMembers.First().Name;

                    onCondition = new SqlEqualityExpression(
                        new SqlColumnExpression(new SqlColumn(left.EntityType.Name, leftKey)),
                        new SqlColumnExpression(new SqlColumn(right.EntityType.Name, rightKey)));
                }
                else
                {
                    onCondition = Edge2SqlExpression(node);
                }
                join = new SqlInnerJoin(join, new SqlTable(node.TypeTree.TableName, node.Type.Name, null), onCondition);
            }

            onCondition = Edge2SqlExpression(endNode);
            var columnExpr = (SqlColumnExpression)onCondition.Rhs;
            // Add type id to the RHS of this onCondition because TypeTree2Sql appends type ids to columns to prevent name clashes
            columnExpr.Column.Name += endNode.TypeTree.TypeId;

            var endTable = new SqlNesting(endNode.TypeTree.ToSql(), endNode.TypeTree.EntityType.Name);
            join = new SqlInnerJoin(join, endTable, onCondition);
            return new SqlSelectFromTable(endTable.AsName, join, null);
        }
        public bool Isloaded { get; internal set; }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            var nodes = (IEnumerable<EntityGraphPathNode>)this;
            while(nodes.Any())
            {
                var root = nodes.First();
                sb.Append(root.Type.Name);
                nodes = nodes.Skip(1);
                if(nodes.Any())
                {
                    sb.Append("->");
                }
            }
            return sb.ToString();
        }

        public void Insert(int index, EntityGraphPathNode item)
        {
            nodes.Insert(index, item);
        }
        public void Add(EntityGraphPathNode item)
        {
            nodes.Add(item);
        }
        public IEnumerator<EntityGraphPathNode> GetEnumerator()
        {
            return nodes.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        public EntityGraphPathNode this[int index]
        {
            get { return nodes[index]; }
            set { nodes[index] = value; }
        }
    }
}
